/* caGenerator.c */

/*
 * Licensed to the ConArtist Development Team (CADT).
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 * The CADT licenses this file to you under the Apache License, 
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <apr_hash.h>
#include <apr_strings.h>
#include <apr_thread_rwlock.h>
#include <apr_fnmatch.h>
#include <apr_file_io.h>
#include <apr_file_info.h>

#include "conartist.h"
#include "caPrivate.h"
#include "caGenerator.h"
#include "caLog.h"
#include "caStore.h"
#include "caMaps.h"

static apr_hash_t *hostHash = NULL;
static apr_hash_t *genHash = NULL;
static apr_thread_rwlock_t *gRWLock = NULL;
static apr_pool_t *genPool = NULL;

struct hostMatch {
    struct hostMatch *next;
    char *hostname;
    apr_port_t port;
    struct caGenerator *generator;
};

struct storeAlias {
    struct caStore *store;
    char *alias;
};

apr_status_t genInit(void)
{
    apr_status_t rv = apr_pool_create(&genPool, globalPool);
    if (rv != APR_SUCCESS)
        return rv;
    hostHash = apr_hash_make(genPool);
    if (!hostHash)
        return APR_ENOMEM;
    genHash = apr_hash_make(genPool);
    if (!genHash)
        return APR_ENOMEM;

    rv = apr_thread_rwlock_create(&gRWLock, genPool);
    if (rv != APR_SUCCESS) {
        apr_pool_destroy(genPool);
        return rv;
    }
    return rv;
}

apr_status_t caGeneratorCreate(struct caGenerator **grv, const char *name)
{
    struct caGenerator *g = NULL;
    apr_status_t rv = APR_EINVAL;

    if (!name)
        return rv;

    g = apr_pcalloc(genPool, sizeof(*g));
    if (!g) {
        caLog(CA_LOG_ERROR, "Unable to allocate memory for generator '%s'",
              name);
        return APR_ENOMEM;
    }

    rv = apr_pool_create(&g->pool, genPool);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_ERROR, "Unable to create a pool for generator '%s'",
              name);
        return APR_ENOMEM;
    }

    /* Make the mapStore */
    rv = caMapStoreCreate(&g->mapStore, g->pool);
    if (rv != APR_SUCCESS)
        return rv;

    /* Make the locks */
    rv = apr_thread_rwlock_create(&g->storeLock, g->pool);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_ERROR, "Unable to create store RW lock for generator '%s'",
              name);
        goto clean_exit;
    }

    /* Make the hashes and tables */
    g->storeHash = apr_hash_make(g->pool);
    if (!g->storeHash)
        goto clean_exit;
    g->piHash = apr_hash_make(g->pool);
    if (!g->piHash)
        goto clean_exit;
    g->cfg = apr_table_make(g->pool, 1);
    if (!g->cfg)
        goto clean_exit;
   
    g->name = apr_pstrdup(g->pool, name);
    apr_table_set(g->cfg, "Name", name);

    rv = apr_thread_rwlock_wrlock(gRWLock);
    if (rv == APR_SUCCESS) {
        apr_hash_set(genHash, g->name, APR_HASH_KEY_STRING, g);
        apr_thread_rwlock_unlock(gRWLock);
    }
    caLog(CA_LOG_INFO, "Created generator '%s'", name);

    *grv = g;
    return APR_SUCCESS;
clean_exit:
    apr_pool_destroy(g->pool);
    return rv;
}

apr_status_t caGeneratorCheckCache(struct caGenerator *g)
{
    char *fspath = NULL;
    apr_status_t rv;

    fspath = (char *)apr_table_get(g->cfg, "CachePath");
    if (! fspath) {
        fspath = (char *)apr_table_get(libConfig, "CachePath");
        if (!fspath) {
            caLog(CA_LOG_WARN, "No CachePath set, defaulting to '/tmp'");
            fspath = "/tmp";
        }

        rv = apr_filepath_merge(&fspath, fspath, g->name, 0, g->pool);
        if (rv != APR_SUCCESS)
            return rv;
        apr_table_set(g->cfg, "CachePath", fspath);
    }

    rv = caRecurseCheckDirectory((fspath[0] == '/' ? "/" : NULL), fspath);
    if (rv != APR_SUCCESS)
        caLog(CA_LOG_ERROR, "Unable to create or use directory '%s'",
              fspath);
    else
        caLog(CA_LOG_INFO, "Created cache path '%s'", fspath);
    return rv;
}

static apr_status_t appendStore(struct caGenerator *g, struct storeAlias *sa)
{
    apr_status_t rv;

    rv = apr_thread_rwlock_wrlock(g->storeLock);
    if (rv != APR_SUCCESS)
        return rv;

    apr_hash_set(g->storeHash, sa->alias, APR_HASH_KEY_STRING, sa);
    apr_thread_rwlock_unlock(g->storeLock);

    caLog(CA_LOG_INFO, "Added store '%s' to generator '%s'",
          sa->alias, g->name);

    return APR_SUCCESS;
}

apr_status_t caGeneratorAddStore(struct caGenerator *g, const char *alias,
                                 const char *storeName)
{
    apr_status_t rv;
    struct caStore *ck = NULL;
    struct caStore *st = NULL;
    struct storeAlias *sa = NULL;

    if (!g || !storeName)
        return APR_EINVAL;

    rv = caStoreFind(&st, storeName);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_WARN, "Unable to find a store called '%s'",
              storeName);
        return rv;
    }

    sa = apr_pcalloc(g->pool, sizeof(*sa));
    if (!sa)
        return APR_ENOMEM;

    sa->alias = alias ? apr_pstrdup(g->pool, alias) : st->name;
    sa->store = st;

    rv = apr_thread_rwlock_rdlock(g->storeLock);
    if (rv != APR_SUCCESS)
        return rv;
    ck = apr_hash_get(g->storeHash, sa->alias, APR_HASH_KEY_STRING);
    apr_thread_rwlock_unlock(g->storeLock);

    if (ck) {
        caLog(CA_LOG_WARN, "Generator '%s' already has a store named "
              "'%s' registered!", g->name, sa->alias);
        return EALREADY;
    }
    return appendStore(g, sa);
}

static struct hostMatch *findHost(const char *hostname, apr_port_t port, 
                                  int exact)
{
    struct hostMatch *hm = NULL, *maybe = NULL;
    apr_status_t rv;

    rv = apr_thread_rwlock_rdlock(gRWLock);
    if (rv != APR_SUCCESS)
        return NULL;
    hm = apr_hash_get(hostHash, hostname, APR_HASH_KEY_STRING);
    if (hm) {
        if (exact == 0)
            maybe = hm;
        for (; hm; hm = hm->next) {
            if (hm->port == port)
                break;
        }
    }
    apr_thread_rwlock_unlock(gRWLock);
    if (hm)
        return hm;
    return maybe;
}

apr_status_t caRegisterHostname(struct caGenerator *g, const char *h,
                                apr_port_t port)
{
    apr_status_t rv;
    apr_ssize_t hLen = 0;
    struct hostMatch *hm = NULL, *hmm = NULL;

    if (!h || !g)
        return APR_EINVAL;
    hLen = strlen(h);

    /* Look for an exact match */
    hm = findHost(h, port, 1);
    if (hm) {
        caLog(CA_LOG_ERROR, "Attempt to register duplicate hostname/port");
        return APR_EINVAL;
    }

    hm = apr_pcalloc(genPool, sizeof(*hm));
    if (!hm)
        return APR_SUCCESS;
    hm->hostname = apr_pstrdup(genPool, h);
    hm->port = port;
    hm->generator = g;

    /* Find first entry in hash for hostname */
    hmm = findHost(h, port, 0);
    rv = apr_thread_rwlock_wrlock(gRWLock);
    if (rv != APR_SUCCESS)
        return APR_EINVAL;

    if (!hmm)
        apr_hash_set(hostHash, hm->hostname, APR_HASH_KEY_STRING, hm);
    else {
        for (; hmm->next; hmm = hmm->next)
            ;
        hmm->next = hm;
    }
    apr_thread_rwlock_unlock(gRWLock);

    caLog(CA_LOG_INFO, "Registered '%s:%d' with generator '%s'",
          h, port, g->name);

    return rv;
}    

apr_status_t caGeneratorFind(struct caGenerator **gen,
                             const char *hostname, apr_port_t port,
                             const char *url)
{
    struct hostMatch *hm = NULL;

    *gen = NULL;

    if (!hostname || !url)
        return APR_EINVAL;

    /* non-exact match required */
    hm = findHost(hostname, port, 0);
    if (!hm)
        return APR_EINVAL;
    *gen = hm->generator;
    return APR_SUCCESS;
}

apr_status_t caGeneratorCacheFilename(char **path, const char *fn,
                                      struct caGenerator *gen)
{
    const char *cPath = NULL;
    apr_status_t rv;

    *path = NULL;

    rv = caGeneratorCheckCache(gen);
    if (rv != APR_SUCCESS)
        return rv;

    cPath = apr_table_get(gen->cfg, "CachePath");
    if (!cPath) {
        caLog(CA_LOG_WARN, "No CachePath set, so defaulting to '/tmp'");
        cPath = "/tmp";
    }
    return apr_filepath_merge(path, cPath, (fn[0] == '/' ? &fn[1] : fn),
                              0, gen->pool);
}

apr_status_t caGeneratorGetStore(struct caStore **store, const char *fn,
                                 struct caGenerator *gen)
{
    char *ptr;
    int pos = 0;
    struct storeAlias *sa = NULL;

    *store = NULL;
    if (!gen) {
        caLog(CA_LOG_ERROR, "Trying to get a store from a NULL generator");
        return APR_EINVAL;
    }
    ptr = strchr(fn, ':');
    if (ptr) {
        pos = ptr - fn;
/* TODO - locking! */
        sa = apr_hash_get(gen->storeHash, fn, pos);
    } else {
        apr_hash_index_t *hi;
        void *saPtr;
        hi = apr_hash_first(gen->pool, gen->storeHash);
        if (hi) {
            apr_hash_this(hi, NULL, NULL, &saPtr);
            sa = saPtr;
        }
    }
    
    if (sa)
        *store = sa->store;
    return APR_SUCCESS;
}

apr_status_t caGeneratorGetFilename(char **path, const char *fn,
                                    struct caGenerator *gen)
{
    apr_status_t rv;
    struct caStore *st = NULL;
    char *ptr = NULL;

    *path = NULL;

    ptr = strchr(fn, ':');
    if (ptr && strncmp(fn, "cache", ptr - fn) == 0)
        return caGeneratorCacheFilename(path, ++ptr, gen);

    rv = caGeneratorGetStore(&st, fn, gen);
    if (rv != APR_SUCCESS)
        return rv;

    if (!st || !st->module->getPath)
        return APR_EINVAL;
    return (st->module->getPath)(path, (ptr ? &ptr[1] : fn), gen->pool,
                                 st->privateData);
}

apr_status_t caGeneratorAddPipeline(struct caGenerator *gen, 
                                    struct caPipeline *pl)
{
    struct pipelineInstance *pi = NULL;
    struct pipelineInstance *ck = NULL;
    apr_status_t rv;

caLog(CA_LOG_DEBUG, "caGeneratorAddPipeline(%s)\n", pl->name);

    rv = apr_thread_rwlock_rdlock(gRWLock);
    if (rv != APR_SUCCESS)
        return rv;
    ck = apr_hash_get(gen->piHash, pl->name, APR_HASH_KEY_STRING);
    apr_thread_rwlock_unlock(gRWLock);
    
    if (ck)
        return APR_EINVAL;

    pi = apr_pcalloc(gen->pool, sizeof(*pi));
    if (!pi)
        return APR_ENOMEM;

    pi->gen = gen;
    pi->pipeline = pl;

    rv = caPipelineInstanceCreatePipes(pi);
    if (rv != APR_SUCCESS)
        return rv;

    rv = apr_thread_rwlock_wrlock(gRWLock);
    if (rv != APR_SUCCESS)
        return rv;
    apr_hash_set(gen->piHash, pl->name, APR_HASH_KEY_STRING, pi);
    apr_thread_rwlock_unlock(gRWLock);

caLog(CA_LOG_DEBUG, "caGeneratorAddPipeline(%s)\n", pl->name);
    return APR_SUCCESS;
}

apr_status_t caGeneratorFindPipeline(struct pipelineInstance **pipe,
                                     const char *name,
                                     struct caGenerator *gen)
{
    struct pipelineInstance *pi = NULL;
    apr_status_t rv;

    rv = apr_thread_rwlock_rdlock(gRWLock);
    if (rv != APR_SUCCESS)
        return rv;
    pi = apr_hash_get(gen->piHash, name, APR_HASH_KEY_STRING);
    apr_thread_rwlock_unlock(gRWLock);

    *pipe = pi;
    return APR_SUCCESS;
}

/* Write file contents into the file. */
static apr_status_t cacheWrite(struct caURIMap **mpp, struct caURIMap *mp,
                               const char *uri, struct caGenerator *gen, 
                               const char *fn, const char *fBuffer,
                               apr_size_t fLen)
{
    struct caURIMap *mmp = NULL;
    apr_status_t rv = APR_SUCCESS;

    if (! mp->generatedFn) {
        rv = cloneURIMap(&mmp, mp, gen, uri, fn);
        if (rv != APR_SUCCESS)
            return rv;
    } else
        mmp = mp;

    /* If we have a buffer containing the file contents, then we write
     * the contents into the file here.
     */
    if (fBuffer)
        rv = caFileWrite(mmp->generatedFn, fBuffer, fLen);
    mmp->fLen = fLen;
    mmp->flags |= CAM_CLEAN;

    if (mpp)
        *mpp = mmp;

    return rv;
}

static void invalidateMP(void *data, const char *which, int what)
{
    struct caURIMap *mp = (struct caURIMap *)data;

    mp->flags &= ~CAM_CLEAN;
    mp->flags |= CAM_DIRTY;
}

static void copyOutputInfo(struct caContent *req, struct caURIMap *map,
                           char *oBuffer, apr_size_t oLen)
{
    req->filename = apr_pstrdup(req->pool, map->generatedFn);
    req->fLen = map->fLen;
    if (map->pi)
        req->contentType = apr_pstrdup(req->pool, 
                                       map->pi->pipeline->contentType);
    /* TODO - encoding? */
    if (oBuffer) {
        req->data = oBuffer;
        req->dataLen = oLen;
    }
}
    
static apr_status_t caGenerateContent(struct caURIMap *mp, 
                                      struct caContent *req,
                                      struct caGenerator *gen)
{
    char *fn = NULL;
    apr_status_t rv;
    struct caStore *st = NULL;
    char *fBuffer = NULL;
    apr_size_t fSz = 0;
    struct caURIMap *mpp = NULL;

    /* TODO - access controls? */

    caLog(CA_LOG_DEBUG, "caGenerateContent(%s) for %s", mp->generatedFn, 
          req->uri);

    /* If we have a generated file that is marke as "clean" we can simply
     * use it. This is a shortcut.
     */
    if (mp->generatedFn && (mp->flags & CAM_CLEAN)) {
        copyOutputInfo(req, mp, NULL, 0);
        caLog(CA_LOG_DEBUG, "Serving '%s' from map cache", req->uri);
        return APR_SUCCESS;
    }

    fn = mapOutputFilename(mp, req->uri, gen->pool);

    rv = caGeneratorGetStore(&st, fn, gen);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_ERROR, "Unable to find the store for path '%s'", fn);
        return rv;
    }

    rv = caFileGet(&fBuffer, &fSz, fn, st, gen->pool);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_ERROR, "Unable to read file '%s'", fn);
        return rv;
    }

    if (mp->pi) {
        struct caPipeData pd;

        /* Create the caPipeData structure! */
        pd.input = fBuffer;
        pd.iLen = fSz;
        pd.pool = req->pool;
        pd.settings = apr_table_make(req->pool, 0);

        rv = caPipelineRun(&pd, mp->pi);
        if (rv != APR_SUCCESS) {
            caLog(CA_LOG_ERROR, "Pipeline '%s' failed to execute fully",
                  mp->pi->pipeline->name);
            return rv;
        }
        fBuffer = pd.output;
        fSz = pd.oLen;
    }

    rv = cacheWrite(&mpp, mp, req->uri, gen, fn, fBuffer, fSz);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_WARN, "Unable to write cache data for '%s'",
              req->uri);
        return rv;
    }

    if (st)
        storeNotifyAdd(st, fn, CAN_ALL, invalidateMP, mpp);

    copyOutputInfo(req, mpp, fBuffer, fSz);
    return APR_SUCCESS;
}

/* Now we try and actually do something :-)
 *
 * This function exists to try and find which mapping section should handle
 * the request that we've received. There are a number of possible
 * ways that the mapping can be specified and so this function must check
 * for all of them and act correctly in all situations - easier said than
 * done!
 */
apr_status_t caGeneratorProcessRequest(struct caGenerator *gen, 
                                       struct caContent *req)
{
    struct caURIMap *mp = NULL;
    apr_status_t rv;

    if (!gen)
        return APR_EINVAL;

    caLog(CA_LOG_DEBUG, "caGeneratorProcessRequest(%s, %s)", gen->name,
          req->uri);

    mp = caGeneratorFindMap(gen, req->uri);
    if (!mp)
        return APR_ENOENT;

    /* Process dependancies, if there are any */
    if (apr_hash_count(mp->deps)) {
        apr_hash_index_t *hi;
        struct caContent depreq;
        caLog(CA_LOG_DEBUG, "Processing dependancies for '%s'", req->uri);
        for (hi = apr_hash_first(gen->pool, mp->deps); hi; hi = apr_hash_next(hi)) {
            const char *dep = NULL;
            const void *depPtr = dep;
            apr_hash_this(hi, &depPtr, NULL, NULL);
            memcpy(&depreq, req, sizeof(depreq));
            depreq.uri = dep;
            rv = caGeneratorProcessRequest(gen, &depreq);
            if (rv != APR_SUCCESS) {
                caLog(CA_LOG_ERROR, "Unable to create dependancy '%s' of "
                      "%s", dep, req->uri);
                return rv;
            }
        }
        caLog(CA_LOG_DEBUG, "Dependancies for '%s' all satisfied", req->uri);
    }

    if (mp)
        return caGenerateContent(mp, req, gen);

    return APR_EINVAL;
}

apr_status_t caGeneratorOpenDir(struct caDir **thedir,
                                const char *path, 
                                struct caGenerator *gen)
{
    apr_status_t rv;
    struct caStore *st;
    char *ptr = NULL;

    rv = caGeneratorGetStore(&st, path, gen);
    if (rv != APR_SUCCESS) {
        caLog(CA_LOG_WARN, "Unable to get a store for '%s'", path);
        return rv;
    }
    ptr = strchr(path, ':');

    *thedir = caOpenDirectory(st, ptr ? ++ptr : path, gen->pool);
    if (*thedir)
        return APR_SUCCESS;
    return APR_EINVAL;
}
